Redes neuronales artificiales (RNA)¶

Una red neuronal artificial (RNA) es un sistema computacional inspirado en el funcionamiento del cerebro humano. Está compuesto por unidades básicas llamadas neuronas artificiales, que se interconectan entre sí formando una red. Estas neuronas artificiales simulan el comportamiento de las neuronas biológicas, recibiendo información, procesándola y transmitiéndola a otras neuronas.

Las redes neuronales artificiales se pueden entrenar para realizar una amplia variedad de tareas, como:

  • Reconocimiento de imágenes: identificar objetos, personas o animales en imágenes.
  • Procesamiento del lenguaje natural: entender y generar lenguaje humano.
  • Traducción automática: traducir texto de un idioma a otro.
  • Predicción: pronosticar eventos futuros, como el precio de las acciones o el clima.
  • Toma de decisiones: ayudar a tomar decisiones complejas, como la aprobación de un crédito o el diagnóstico de una enfermedad.

Las redes neuronales artificiales se han convertido en una herramienta fundamental en el campo de la inteligencia artificial, con aplicaciones en diversos sectores como la medicina, las finanzas, la industria y el marketing.

Características de las redes neuronales artificiales:

  • Están compuestas por neuronas artificiales interconectadas.
  • Las neuronas artificiales simulan el comportamiento de las neuronas biológicas.
  • Se pueden entrenar para realizar una amplia variedad de tareas.
  • Son una herramienta fundamental en el campo de la inteligencia artificial.

Ejemplos de cómo se utilizan las redes neuronales artificiales en la actualidad:

  • Reconocimiento facial: Los teléfonos inteligentes y las cámaras de seguridad utilizan redes neuronales artificiales para reconocer rostros.
  • Asistentes virtuales: Los asistentes virtuales como Siri, Alexa y Google Assistant utilizan redes neuronales artificiales para entender el lenguaje natural y responder a las preguntas de los usuarios.
  • Coches autónomos: Los coches autónomos utilizan redes neuronales artificiales para navegar por las carreteras y evitar obstáculos.
  • Detección de fraudes: Las empresas financieras utilizan redes neuronales artificiales para detectar transacciones fraudulentas.

Las redes neuronales artificiales (RNA) se pueden usar en la analítica empresarial para una amplia variedad de tareas, incluyendo:

1. Segmentación de clientes: Las RNA pueden usarse para identificar grupos de clientes con características similares, lo que permite a las empresas personalizar sus ofertas y estrategias de marketing.

2. Predicción de la demanda: Las RNA pueden usarse para pronosticar la demanda de productos o servicios, lo que ayuda a las empresas a optimizar sus inventarios y recursos.

3. Detección de fraude: Las RNA pueden usarse para detectar transacciones fraudulentas, lo que ayuda a las empresas a protegerse de pérdidas financieras.

4. Análisis de riesgos: Las RNA pueden usarse para evaluar los riesgos asociados con diferentes decisiones comerciales, lo que ayuda a las empresas a tomar decisiones más informadas.

5. Optimización de precios: Las RNA pueden usarse para determinar el precio óptimo para productos o servicios, lo que ayuda a las empresas a maximizar sus ganancias.

6. Análisis del sentimiento del cliente: Las RNA pueden usarse para analizar las opiniones de los clientes en las redes sociales y otras plataformas, lo que ayuda a las empresas a comprender mejor las necesidades y expectativas de sus clientes.

7. Automatización de tareas: Las RNA pueden usarse para automatizar tareas repetitivas, como la clasificación de documentos o la extracción de datos, lo que libera tiempo para que los empleados se concentren en tareas más estratégicas.

8. Detección de anomalías: Las RNA pueden usarse para detectar anomalías en los datos, como picos repentinos en las ventas o cambios en el comportamiento del cliente, lo que puede ayudar a las empresas a identificar problemas potenciales y tomar medidas correctivas.

9. Personalización de recomendaciones: Las RNA pueden usarse para recomendar productos o servicios a los clientes en función de sus intereses y preferencias, lo que aumenta la satisfacción del cliente y las ventas.

10. Mejora de la atención al cliente: Las RNA pueden usarse para crear chatbots que puedan responder a las preguntas de los clientes y brindar asistencia, lo que mejora la experiencia del cliente y reduce los costos de atención al cliente.

En resumen, las RNA son una herramienta poderosa que puede usarse para mejorar la analítica empresarial en una variedad de maneras. Al aprovechar el poder de las RNA, las empresas pueden obtener información valiosa de sus datos, tomar decisiones más informadas y mejorar sus resultados.

Uso de las RNA en la analítica empresarial:

  • Netflix: Netflix utiliza las RNA para recomendar películas y series a sus usuarios.
  • Amazon: Amazon utiliza las RNA para predecir la demanda de productos y optimizar sus inventarios.
  • PayPal: PayPal utiliza las RNA para detectar transacciones fraudulentas.
  • American Express: American Express utiliza las RNA para analizar el riesgo de crédito de los solicitantes de tarjetas de crédito.

Las RNA son una tecnología en constante evolución con un gran potencial para la analítica empresarial. A medida que las RNA continúen desarrollándose, podemos esperar ver aún más aplicaciones innovadoras en el futuro.

Unidad básica de cómputo¶

Una neurona artificial, también conocida como neurona formal o perceptrón, es un modelo matemático simple que simula el comportamiento de una neurona biológica. Es la unidad fundamental de las redes neuronales artificiales (RNA).

drawing

Funcionamiento:

  1. Entradas: La neurona recibe señales de otras neuronas, llamadas entradas. Estas señales pueden ser valores numéricos, como 0 o 1, o valores reales, como 0.5 o 2.3.
  2. Pesos: Cada entrada tiene un peso asociado que indica la importancia de esa entrada para la neurona. Los pesos pueden ser positivos o negativos.
  3. Suma ponderada: La neurona multiplica cada entrada por su peso y luego suma todos los productos.
  4. Función de activación: La neurona aplica una función matemática, llamada función de activación, a la suma ponderada. La función de activación determina la salida de la neurona.
  5. Salida: La salida de la neurona es un valor numérico que puede ser 0 o 1, o un valor real dentro de un rango específico.

Ejemplo:

Supongamos que tenemos una neurona artificial con tres entradas $ x_1, x_2, x_3 $ y sus respectivos pesos $ w_1, w_2, w_3 $. La neurona también tiene un sesgo (bias) $ b = w_b \times 1$. La función de activación que utilizaremos es la función sigmoide, que tiene la siguiente fórmula:

$$ f(z) = \frac{1}{1 + e^{-z}} $$

Donde $ z $ es la suma ponderada de las entradas y los pesos más el sesgo:

$$ z = x_1 \times w_1 + x_2 \times w_2 + x_3 \times w_3 + b $$
  1. Entradas y Pesos: Supongamos que tenemos:

    • $ x_1 = 0.5 $, $ x_2 = 0.3 $, $ x_3 = 0.8 $
    • $ w_1 = 0.2 $, $ w_2 = 0.4 $, $ w_3 = 0.7 $
    • $ b = -0.3 $
  2. Suma ponderada: Calculamos $ z $: $$ z = 0.5 \times 0.2 + 0.3 \times 0.4 + 0.8 \times 0.7 - 0.3 = 0.1 + 0.12 + 0.56 - 0.3 = 0.48 $$

  3. Función de activación: Ahora, aplicamos la función sigmoide a $ z $: $$ f(z) = \frac{1}{1 + e^{-0.48}} \approx \frac{1}{1 + e^{-0.48}} \approx \frac{1}{1 + 0.618} \approx \frac{1}{1.618} \approx 0.618 $$

La salida de esta neurona artificial, utilizando la función de activación sigmoide, sería aproximadamente $ 0.618 $.

Esta salida se puede utilizar como entrada para otras neuronas en una red neuronal más grande o como salida final, dependiendo de la arquitectura y la tarea específica que esté realizando la red neuronal.

Funciones de activación¶

Las funciones de activación son elementos fundamentales en las redes neuronales artificiales, ya que introducen no linealidades en el modelo, permitiendo que la red pueda aprender y modelar relaciones complejas en los datos. A continuación tienes una descripción de algunas de las funciones de activación más importantes:

  1. Función Sigmoide:

    • Fórmula: $ f(x) = \frac{1}{1 + e^{-x}} $
    • Rango: $ (0, 1) $
    • Características:
      • Produce una salida suave y continua que se aproxima a 0 para entradas grandes negativas y a 1 para entradas grandes positivas.
      • Es útil en capas ocultas de redes neuronales cuando se necesita una salida entre 0 y 1.
      • Se utiliza comúnmente en redes neuronales para tareas de clasificación binaria.
  2. Función ReLU (Rectified Linear Unit):

    • Fórmula: $ f(x) = \max(0, x) $
    • Rango: $[0, +\infty)$
    • Características:
      • Produce una salida cero para entradas negativas y mantiene la entrada para valores positivos.
      • Es simple y computacionalmente eficiente.
      • Ayuda a evitar el problema de desvanecimiento del gradiente.
      • Es muy utilizada en capas ocultas de redes neuronales, acelerando el proceso de entrenamiento.
  3. Función Tangente Hiperbólica (Tanh):

    • Fórmula: $ f(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}} $
    • Rango: $(-1, 1)$
    • Características:
      • Es similar a la función sigmoide, pero produce salidas en un rango de -1 a 1.
      • Útil en capas ocultas de redes neuronales cuando se necesita una salida en un rango simétrico alrededor de cero.
  4. Función Lineal:

    • Fórmula: $ f(x) = x $
    • Rango: $(-\infty, +\infty)$
    • Características:
      • Produce una salida que es igual a la entrada.
      • Es adecuada para modelos de regresión cuando la salida puede tomar cualquier valor real.
  5. Función Softmax:

    • Fórmula: $ f(x_i) = \frac{e^{x_i}}{\sum_{j=1}^{n} e^{x_j}} $
    • Rango: $ (0, 1) $ (La suma de todas las salidas es 1)
    • Características:
      • Se utiliza comúnmente en la capa de salida de redes neuronales para tareas de clasificación multiclase.
      • Normaliza las salidas para que sumen 1, lo que las hace interpretables como probabilidades.

Estas son algunas de las funciones de activación más importantes y utilizadas en redes neuronales artificiales. La elección de la función de activación adecuada depende de la naturaleza de la tarea y la arquitectura de la red neuronal.

Red neuronal feedforward¶

Una red neuronal feedforward, también conocida como perceptrón multicapa o red neuronal de propagación hacia adelante, es uno de los tipos más simples y comunes de redes neuronales artificiales.

  1. Arquitectura: Una red neuronal feedforward está compuesta por una serie de capas de neuronas, que están dispuestas en forma de grafo acíclico dirigido. Las neuronas en una capa están conectadas con todas las neuronas de la capa siguiente, pero no hay conexiones hacia atrás, lo que significa que la información fluye en una sola dirección, de entrada a salida.

  2. Capas: La red consta de tres tipos de capas:

    • Capa de entrada: Donde se introducen los datos o características del problema que se está abordando.
    • Capas ocultas: Capas intermedias entre la capa de entrada y la capa de salida. Cada neurona en estas capas está conectada con todas las neuronas de la capa anterior y la siguiente. Estas capas son responsables de aprender y extraer características relevantes de los datos.
    • Capa de salida: Donde se produce la salida final de la red. Dependiendo del tipo de problema, puede haber una sola neurona en esta capa (para tareas de regresión) o múltiples neuronas (para tareas de clasificación o regresión multiclase).
  3. Propagación hacia adelante (Forward Propagation): El proceso de propagación hacia adelante comienza con la introducción de los datos en la capa de entrada. Cada neurona en cada capa oculta y de salida calcula una combinación lineal de las entradas ponderadas por los pesos de las conexiones y luego aplica una función de activación a esta combinación lineal para producir la salida de la neurona.

  4. Funciones de activación: Como mencionado anteriormente, cada neurona en la red utiliza una función de activación para introducir no linealidades en el modelo. Las funciones de activación más comunes son la función sigmoide, ReLU (Rectified Linear Unit), tanh (tangente hiperbólica) y softmax (en la capa de salida para tareas de clasificación multiclase).

  5. Entrenamiento: El entrenamiento de la red neuronal feedforward se realiza utilizando algoritmos de optimización como el descenso del gradiente estocástico (SGD) o sus variantes, donde se ajustan los pesos de las conexiones para minimizar una función de pérdida que mide la discrepancia entre las salidas predichas por la red y las salidas reales.

  6. Retropropagación (Backpropagation): Una vez calculada la pérdida, se propaga hacia atrás a través de la red utilizando el algoritmo de retropropagación, ajustando los pesos de las conexiones en función del gradiente de la función de pérdida con respecto a los pesos. Este proceso se repite iterativamente durante múltiples épocas hasta que la red converge y la pérdida se minimiza.

Una red neuronal feedforward funciona propagando la información hacia adelante a través de múltiples capas de neuronas, calculando las salidas en cada capa mediante combinaciones lineales de las entradas ponderadas por los pesos y aplicando funciones de activación no lineales. Luego, se ajustan los pesos durante el entrenamiento para minimizar la pérdida utilizando algoritmos de optimización como el descenso del gradiente estocástico y la retropropagación.

drawing

Propagación hacia adelante (Forward Propagation)¶

La propagación feedforward de manera matricial para una capa oculta en un MLP es la siguiente:

$$ Z^l = W^l \cdot A^{l-1} + B^l $$$$ A^l = \sigma(Z^l) $$

Donde:

  • $ l $ es el índice de la capa.
  • $ Z^l $ es el vector de entrada neta de todas las neuronas en la capa $ l $.
  • $ W^l $ es la matriz de pesos de todas las conexiones entre las neuronas en la capa $ l $ y las neuronas en la capa $ l-1 $.
  • $ A^{l-1} $ es el vector de salida de todas las neuronas en la capa $ l-1 $.
  • $ B^l $ es el vector de sesgos (biases) de todas las neuronas en la capa $ l $.
  • $ \sigma $ es la función de activación aplicada elemento por elemento al vector $ Z^l $.
  • $ A^l $ es el vector de salida de todas las neuronas en la capa $ l $.

Para la capa de salida, la fórmula es similar pero sin aplicar la función de activación si la salida es lineal, o aplicando la función de activación adecuada si la salida es no lineal.

Esta formulación matricial permite realizar eficientemente el cálculo del feedforward utilizando operaciones matriciales y de vectores, lo que es especialmente útil en implementaciones computacionales eficientes de MLPs.

Retropropagación (backpropagation)¶

La retropropagación (backpropagation) es el algoritmo fundamental utilizado para entrenar redes neuronales mediante el cálculo eficiente de los gradientes de la función de pérdida con respecto a los pesos de la red. Este algoritmo se basa en el principio del descenso del gradiente, donde los pesos se actualizan en la dirección opuesta al gradiente de la función de pérdida con respecto a los pesos, con el objetivo de minimizar la pérdida.

A continuación se detalla el proceso de retropropagación paso a paso, incluyendo la matemática involucrada:

Supongamos que tenemos una red neuronal feedforward con múltiples capas, y queremos entrenarla para una tarea de clasificación. Sea $ L $ la función de pérdida que queremos minimizar, que generalmente se define como la diferencia entre las salidas predichas ($ \hat{y} $) y las salidas reales ($ y $).

  1. Propagación hacia adelante (Forward Propagation):

    • Para un conjunto de datos de entrada $ X $, calculamos las salidas de cada neurona en la red neuronal utilizando las entradas ponderadas por los pesos y aplicando las funciones de activación en cada capa.
    • Definimos la salida de la red como $ \hat{y} = f(x; \theta) $, donde $ \theta $ representa todos los parámetros de la red (pesos y sesgos).
  2. Cálculo de la pérdida (Loss Calculation):

    • Calculamos la función de pérdida $ L(\hat{y}, y) $ que mide la discrepancia entre las salidas predichas y las salidas reales.
  3. Retropropagación (Backpropagation):

    • Calculamos el gradiente de la función de pérdida con respecto a los pesos de la red utilizando la regla de la cadena y el algoritmo de diferenciación automática (autograd en bibliotecas como PyTorch o TensorFlow).
    • Para una capa dada, el gradiente de la función de pérdida con respecto a los pesos $ \theta $ se calcula utilizando el gradiente de la función de activación $ \frac{\partial f}{\partial z} $ (donde $ z $ es la entrada a la función de activación), y el gradiente de $ z $ con respecto a los pesos $ \frac{\partial z}{\partial \theta} $.
    • Los gradientes se propagan hacia atrás a través de la red, calculando el gradiente de la pérdida con respecto a los pesos de cada capa.
  4. Actualización de pesos (Weight Update):

    • Utilizamos los gradientes calculados en la etapa de retropropagación para actualizar los pesos de la red utilizando un algoritmo de optimización como el descenso del gradiente estocástico (SGD) o sus variantes.
    • La actualización de los pesos se realiza mediante la regla general del descenso del gradiente: $ \theta_{nuevo} = \theta_{viejo} - \alpha \cdot \nabla_{\theta} L $, donde $ \alpha $ es la tasa de aprendizaje y $ \nabla_{\theta} L $ es el gradiente de la función de pérdida con respecto a los pesos.

Este proceso de propagación hacia adelante, cálculo de la pérdida, retropropagación y actualización de pesos se repite iterativamente durante múltiples épocas de entrenamiento hasta que la función de pérdida converge y los pesos de la red se ajustan adecuadamente para la tarea dada.

La retropropagación es el algoritmo clave para entrenar redes neuronales mediante el cálculo eficiente de los gradientes de la función de pérdida con respecto a los pesos de la red, lo que permite ajustar los pesos para minimizar la pérdida y mejorar el rendimiento de la red en la tarea específica.

Gradiente¶

El gradiente es un concepto fundamental en el cálculo vectorial que se utiliza ampliamente en diversos campos, incluyendo matemáticas, física, ingeniería y aprendizaje automático. En el contexto del aprendizaje automático y la optimización de funciones, el gradiente se refiere a la dirección y la tasa de cambio más pronunciada de una función multivariable en un punto dado.

Formalmente, el gradiente de una función escalar $ f $ de $ n $ variables, denotada como $ \nabla f $ o $ \frac{\partial f}{\partial \mathbf{x}} $, es un vector que apunta en la dirección donde la función crece más rápidamente, y su magnitud indica la tasa de cambio en esa dirección. Matemáticamente, el gradiente se define como un vector compuesto de las derivadas parciales de la función con respecto a cada una de las variables independientes.

Para una función $ f(\mathbf{x}) $ donde $ \mathbf{x} = (x_1, x_2, ..., x_n) $ es un vector de variables independientes, el gradiente se calcula como:

$$ \nabla f = \left( \frac{\partial f}{\partial x_1}, \frac{\partial f}{\partial x_2}, ..., \frac{\partial f}{\partial x_n} \right) $$

En términos más simples, el gradiente indica la dirección en la cual la función aumenta más rápidamente. Por lo tanto, en el contexto de la optimización de funciones, el gradiente se utiliza para encontrar los mínimos o máximos locales de una función. En el aprendizaje automático, especialmente en algoritmos de optimización como el descenso del gradiente, el gradiente de la función de pérdida con respecto a los parámetros del modelo se utiliza para ajustar los parámetros de manera iterativa, buscando minimizar la función de pérdida y mejorar el rendimiento del modelo.

Problemas de clasficación¶

Una red neuronal feedforward se puede utilizar para problemas de clasificación mediante un proceso de entrenamiento donde la red aprende a asignar entradas a diferentes categorías o clases.

A continuación se detalla cómo funciona una red feedforward para un problema de clasificación:

  1. Preparación de datos: Primero, necesitas preparar tus datos de entrenamiento. Esto incluye dividir tus datos en conjuntos de entrenamiento, validación y prueba, así como realizar cualquier preprocesamiento necesario, como normalización de características.

  2. Diseño de la red neuronal:

    • Define la arquitectura de la red, incluyendo el número de capas ocultas y el número de neuronas en cada capa.
    • Para la capa de entrada, el número de neuronas debe coincidir con el número de características en tus datos.
    • Para la capa de salida, el número de neuronas debe coincidir con el número de clases en tu problema de clasificación. Por ejemplo, si tienes 3 clases, tendrás 3 neuronas en la capa de salida.

drawing

  1. Inicialización de pesos: Inicializa los pesos de la red de manera aleatoria o utilizando algún método de inicialización de pesos como Xavier o He.

  2. Propagación hacia adelante (Forward Propagation):

    • Propaga los datos de entrada a través de la red neuronal. Esto implica calcular la salida de cada neurona en cada capa utilizando las entradas ponderadas por los pesos y aplicando funciones de activación.
    • La salida de la capa de salida representará las predicciones de la red para cada clase.
  3. Cálculo de la pérdida: Calcula la función de pérdida, que mide la discrepancia entre las salidas predichas por la red y las clases reales de tus datos de entrenamiento. Para problemas de clasificación, una función de pérdida común es la entropía cruzada categórica (categorical cross-entropy).

  4. Retropropagación (Backpropagation):

    • Utiliza el algoritmo de retropropagación para calcular el gradiente de la función de pérdida con respecto a los pesos de la red.
    • Ajusta los pesos de la red en la dirección opuesta al gradiente utilizando un algoritmo de optimización como el descenso del gradiente estocástico (SGD) o Adam.
  5. Validación y ajuste de hiperparámetros:

    • Utiliza el conjunto de validación para ajustar los hiperparámetros de la red, como la tasa de aprendizaje, el tamaño del lote, el número de capas y neuronas, etc.
    • Evalúa el rendimiento de la red en el conjunto de validación para evitar el sobreajuste y mejorar el rendimiento general.
  6. Prueba del modelo:

    • Finalmente, evalúa el rendimiento del modelo entrenado en el conjunto de prueba para obtener una estimación imparcial de su rendimiento en datos no vistos.

Este proceso se repite iterativamente durante múltiples épocas de entrenamiento hasta que la función de pérdida converja y la red neuronal aprenda a realizar clasificaciones precisas en el conjunto de entrenamiento y generalice bien a datos no vistos.

Problemas de regresión¶

Para un problema de regresión, donde el objetivo es predecir un valor numérico continuo en lugar de una categoría, puedes utilizar una red neuronal feedforward de manera similar, pero con algunas diferencias en la arquitectura de la red y la función de pérdida.

Funcionamiento de una red feedforward para un problema de regresión:

  1. Preparación de datos: Al igual que en el caso de clasificación, necesitas preparar tus datos de entrenamiento, dividirlos en conjuntos de entrenamiento, validación y prueba, y realizar cualquier preprocesamiento necesario, como normalización de características.

  2. Diseño de la red neuronal:

    • Define la arquitectura de la red, incluyendo el número de capas ocultas y el número de neuronas en cada capa.
    • Para la capa de entrada, el número de neuronas debe coincidir con el número de características en tus datos.
    • Para la capa de salida, tendrás una sola neurona, ya que estás realizando una regresión, y esta neurona producirá la predicción numérica continua.

drawing

  1. Inicialización de pesos: Inicializa los pesos de la red de manera aleatoria o utilizando algún método de inicialización de pesos.

  2. Propagación hacia adelante (Forward Propagation):

    • Propaga los datos de entrada a través de la red neuronal, calculando la salida de cada neurona en cada capa utilizando las entradas ponderadas por los pesos y aplicando funciones de activación.
    • La salida de la capa de salida será la predicción numérica continua.
  3. Cálculo de la pérdida: Calcula la función de pérdida, que mide la discrepancia entre las salidas predichas por la red y los valores reales de tus datos de entrenamiento. Para problemas de regresión, una función de pérdida común es el error cuadrático medio (Mean Squared Error, MSE).

  4. Retropropagación (Backpropagation):

    • Utiliza el algoritmo de retropropagación para calcular el gradiente de la función de pérdida con respecto a los pesos de la red.
    • Ajusta los pesos de la red en la dirección opuesta al gradiente utilizando un algoritmo de optimización como el descenso del gradiente estocástico (SGD) o Adam.
  5. Validación y ajuste de hiperparámetros:

    • Utiliza el conjunto de validación para ajustar los hiperparámetros de la red, como la tasa de aprendizaje, el tamaño del lote, el número de capas y neuronas, etc.
    • Evalúa el rendimiento de la red en el conjunto de validación para evitar el sobreajuste y mejorar el rendimiento general.
  6. Prueba del modelo:

    • Finalmente, evalúa el rendimiento del modelo entrenado en el conjunto de prueba para obtener una estimación imparcial de su rendimiento en datos no vistos.

Este proceso se repite iterativamente durante múltiples épocas de entrenamiento hasta que la función de pérdida converja y la red neuronal aprenda a realizar predicciones precisas de valores numéricos continuos en el conjunto de entrenamiento y generalice bien a datos no vistos.

MLP para el problema XOR¶

El problema XOR es un desafío clásico en el ámbito del aprendizaje automático y las redes neuronales. XOR es una operación lógica que toma dos entradas binarias y devuelve verdadero (1) si solo una de las entradas es verdadera (1), y falso (0) en cualquier otro caso. La tabla de verdad de la operación XOR es la siguiente:

Entrada X1 Entrada X2 Salida XOR, y
0 0 0
0 1 1
1 0 1
1 1 0

A simple vista, parece un problema sencillo de resolver manualmente. Sin embargo, el desafío surge cuando intentamos utilizar un único perceptrón (una sola neurona) para aprender y predecir la salida de la operación XOR. Esto se debe a que la relación entre las entradas y las salidas no es linealmente separable.

Para resolver el problema XOR utilizando un único perceptrón, necesitaríamos una función de activación no lineal, como la función sigmoide. Sin embargo, incluso con la función sigmoide, un único perceptrón no puede modelar correctamente la operación XOR.

La solución al problema XOR implica el uso de una arquitectura de red neuronal más compleja, como un Perceptrón Multicapa (MLP). Con un MLP, que consta de al menos una capa oculta, podemos capturar y aprender las relaciones no lineales entre las entradas y las salidas, lo que nos permite resolver el problema XOR con alta precisión.

Resultados XOR¶

  • Un modelo simple (menos neuronas en la capa oculta) resulta en una superficie de decisión más simple:

drawing

  • Incrementamos las neuronas en la capa oculta y el resultado es un modelo (y superficie de decisión) más complejos:

drawing

  • Modelos más simples son siempre preferibles
In [1]:
# import regressor
from sklearn.neural_network import MLPRegressor
# import classifier
from sklearn.neural_network import MLPClassifier

from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix, classification_report

# import the KNNimputer class
from sklearn.impute import KNNImputer
# import the StandardScaler
from sklearn.preprocessing import StandardScaler

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.metrics import mean_squared_error
import ipywidgets as widgets
import ipywidgets
import warnings
warnings.filterwarnings('ignore')

from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
from matplotlib import cm
from matplotlib.ticker import LinearLocator, FormatStrFormatter

# Import linear regression model
from sklearn.linear_model import LinearRegression
# Min max scaler
from sklearn.preprocessing import MinMaxScaler
In [2]:
def mlp(x_input, y_output, hidden_neurons, training_steps=5000, lr=0.7, batch_size=1):

    # Input size (N_i: input neurons), and output classes (N_o: output neurongs, size of the output)
    N_i, N_o = x_input.shape[0], y_output.shape[0]

    # Initialize randomly the weights
    # Hidden layer
    w_h = np.random.rand(hidden_neurons+1,N_i) - 0.5
    # Output layer
    w_o=np.random.rand(N_o,hidden_neurons+1) - 0.5

    mse = []

    for ti in range(training_steps):
        # Select training pattern randomly
        #i = np.floor(4*np.random.rand()).astype('int')
        for bi in range(batch_size):
            # Feed-forward the input to hidden layer
            i = np.floor(np.shape(x_input)[1]*np.random.rand()).astype('int')
            r_h = 1 / (1 + np.exp(-w_h*x_input[:,i]))
            r_h[-1] = 1  # Bias from hidden to output layer
            #r_h = np.concatenate((r_h, np.ones((1,1))), axis=0)
            # Feed-forward the input to the output layer
            r_o = 1 / (1 + np.exp(-w_o*r_h))
            # Calculate the network error
            d_o = np.multiply(np.multiply(r_o, 1-r_o), y_output[:,i] - r_o)
            # Calculate the responsibility of the hidden network in the error
            d_h = np.multiply(np.multiply(r_h, (1-r_h)), (w_o.T*d_o))
            # Update weights
            w_o = w_o + lr*(r_h*d_o.T).T
            w_h = w_h + lr*(x_input[:,i]*d_h.T).T

        # Test all patterns
        rht = 1 / (1 + np.exp(-w_h*x_input))
        rht[-1] = 1
        r_o_test = 1 / (1 + np.exp(-w_o*rht))
        mse += [mean_squared_error(np.array(y_output), np.array(r_o_test))]

    return mse, r_o_test, w_h, w_o

def mlp_predict(x_input, w_h, w_o):
    # Test all patterns
    #r_o_test = 1 / (1 + np.exp(-w_o*(1/(1+np.exp(-w_h*r_i)))))
    rht = 1 / (1 + np.exp(-w_h*x_input))
    rht[-1] = 1  # bias
    r_o_test = 1 / (1 + np.exp(-w_o*rht))
    return r_o_test


# XOR input: last neuron always one is the bias: 1 1 1 1
x_input = np.matrix('0 1 0 1; 0 0 1 1; 1 1 1 1')

# XOR output
# Two classes encoding
#y_output = np.matrix('0 1 0 1; 0 0 1 1; 1 1 1 1')

# XOR Output
y_output = np.matrix('0 1 1 0')  # internally is a regression problem

#plt.figure(figsize=(12,6))


@ipywidgets.interact
def plot(hidden = [2, 4, 10, 20],
         epochs = [5000, 1000, 10000],
         learning_rate = [0.7, 0.1, 0.3, 0.5, 0.9],
         plot3d=False
        ):

    fig = plt.figure(constrained_layout=True, figsize=(12,6))
    gs = fig.add_gridspec(1, 3)
    ax1 = fig.add_subplot(gs[0])
    ax2 = fig.add_subplot(gs[1:])
    result = mlp(x_input, y_output, hidden_neurons=hidden,
                 training_steps=epochs, lr=learning_rate, batch_size=1)
    ax1.plot(result[0])
    ax1.set_xlabel("epochs")
    ax1.set_ylabel("Loss (MSE)")

    x_min, x_max = -0.1, 1.1
    y_min, y_max = -0.1, 1.1

    gX = np.linspace(x_min, x_max, 100)
    gY = np.linspace(y_min, y_max, 100)

    gData = np.concatenate((gX.reshape(-1,1), gY.reshape(-1,1)), axis=1)

    # Input, ones is the biased
    gData = np.concatenate((gData, np.ones((100,1))), axis=1)
    x_input_s = np.matrix(gData.transpose())

    y_test = mlp_predict(x_input_s, result[2], result[3])

    xx, yy = np.meshgrid(np.linspace(x_min, x_max, 100),
                         np.linspace(y_min, y_max, 100))

    Xgd = gData[:,0]
    Ygd = gData[:,1]

    Z = []
    for xi in Xgd:
        for yi in Ygd:
            z_result = mlp_predict(np.array([[xi, yi, 1]]).T, result[2], result[3])
            #Z += [np.where(z_result == z_result.max())[0][0]]
            Z += [np.array(z_result[0]).flatten()[0]]

    Z = np.array(Z)
    Z = Z.reshape(xx.shape)

    # Plot contour
    cs = ax2.contourf(xx, yy, Z, cmap=plt.cm.RdYlBu)
    csl = ax2.contour(xx, yy, Z, cmap=plt.cm.RdYlBu)

    # Make a colorbar for the cs returned by the contourf call.
    cbar = plt.colorbar(cs, ax=ax2)
    cbar.ax.set_ylabel('Network output')
    # Add the contour line levels to the colorbar
    cbar.add_lines(csl)

    # Plot the training points
    data = pd.DataFrame(x_input[:-1].T, columns=["X1", "X2"])
    data["y"] =y_output.T
    data.y =  pd.Categorical(data.y)

    sns.scatterplot(data=data, x="X1", y="X2", hue="y", palette=["b", "g"], ax=ax2, s=100)

    plt.suptitle("Decision surface of a mlp classifier")
    plt.legend(loc='center right', borderpad=0, handletextpad=0)
    plt.axis("tight")
    plt.show()

    if plot3d:
        fig = plt.figure()
        ax = fig.add_subplot(projection = '3d')

        surf = ax.plot_surface(xx, yy, Z, cmap=plt.cm.RdYlBu,
                            linewidth=0, antialiased=False)

        ax.view_init(30, -60)
        ax.set_xlabel('X')
        ax.set_ylabel('Y')
        ax.set_zlabel('Network response')

        plt.show()
interactive(children=(Dropdown(description='hidden', options=(2, 4, 10, 20), value=2), Dropdown(description='e…

Superficie de decisión¶

Una superficie de decisión, también conocida como frontera de decisión o límite de decisión, es una representación visual en un espacio de características de cómo un modelo de aprendizaje automático divide o clasifica diferentes clases o categorías de datos. Esta superficie o límite separa las regiones del espacio de características en las que el modelo predice una clase u otra.

En problemas de clasificación binaria, la superficie de decisión divide el espacio de características en dos regiones: una región donde se predice una clase y otra donde se predice la otra clase. Para problemas de clasificación con múltiples clases, habrá múltiples límites de decisión que separan las diferentes clases.

La forma y la complejidad de la superficie de decisión pueden variar según el modelo utilizado y la complejidad del problema. Por ejemplo:

  • En un problema de clasificación lineal, como la regresión logística o la SVM lineal, la superficie de decisión es lineal y se puede representar como un hiperplano en el espacio de características.
  • En problemas de clasificación no lineales, como el XOR, la superficie de decisión puede ser no lineal y más compleja. Un Perceptrón Multicapa (MLP) puede aprender una superficie de decisión no lineal al introducir capas ocultas con funciones de activación no lineales.
  • En problemas de clasificación con datos de alta dimensionalidad, la superficie de decisión puede ser más compleja y difícil de visualizar, pero sigue dividiendo el espacio de características en regiones que corresponden a diferentes clases.

Visualizar la superficie de decisión puede ser útil para comprender cómo un modelo está haciendo sus predicciones y para identificar posibles problemas como el sobreajuste. Sin embargo, en problemas de alta dimensionalidad, la visualización directa de la superficie de decisión puede no ser práctica, y pueden ser necesarias técnicas de reducción de dimensionalidad o visualización alternativas.

Problema de regresión¶

  • Vamos a construir una regresión lineal simple y contrastar el modelo contra un perceptrón multicapa.

  • Variables:

    • $X$: altura en pulgadas.
    • $y$: peso en libras.
  • En particular en este ejercicio queremos demostrar el sobreajuste de un modelo MLP.

    • Los datos tienen una relación positiva y lineal.
    • Una regresión lineal simple es el mejor modelo en este caso, este es nuestro modelo base con el que compararemos nuestro MLP.

Sobreajuste (overfitting)¶

El sobreajuste, o overfitting en inglés, es un fenómeno común en el aprendizaje automático que ocurre cuando un modelo se ajusta demasiado bien a los datos de entrenamiento, hasta el punto de capturar el ruido o las fluctuaciones aleatorias en los datos en lugar de aprender las relaciones subyacentes. Como resultado, el modelo puede tener un rendimiento deficiente cuando se enfrenta a datos nuevos o no vistos, lo que significa que no generaliza bien.

El sobreajuste puede ocurrir cuando el modelo es demasiado complejo en relación con la cantidad de datos disponibles para el entrenamiento. Algunos signos comunes de sobreajuste incluyen:

  1. Bajo rendimiento en datos de prueba: A pesar de tener un rendimiento excelente en los datos de entrenamiento, el modelo muestra un rendimiento deficiente en datos de prueba o validación que no ha visto durante el entrenamiento.

  2. Alta varianza: El modelo es altamente sensible a pequeñas variaciones en los datos de entrenamiento, lo que resulta en diferentes resultados cuando se entrena con diferentes subconjuntos de datos.

  3. Coeficientes excesivamente grandes: En modelos lineales, los coeficientes de las características pueden volverse muy grandes para ajustarse a los datos de entrenamiento, lo que indica que el modelo está capturando el ruido en lugar de la señal.

  4. Curva de aprendizaje divergente: Las curvas de aprendizaje del modelo pueden mostrar una divergencia entre el error de entrenamiento y el error de validación, donde el error de entrenamiento continúa disminuyendo mientras que el error de validación comienza a aumentar.

Para evitar el sobreajuste, es importante utilizar técnicas como la regularización, la validación cruzada, el conjunto de validación y el ajuste de hiperparámetros para controlar la complejidad del modelo y garantizar que generalice bien a datos no vistos. Estas técnicas ayudan a encontrar el equilibrio adecuado entre el sesgo y la varianza del modelo, lo que conduce a un mejor rendimiento general en datos nuevos.

Resultados para una regresión simple¶

  • Podemos observar (en el panel izquierdo) que para 10 neuronas en la capa oculta, obtenemos un modelo de MLP (rojo), cercano a la curva de regresión lineal (verde).

    • El MLP tienen un error más grande, no es nuestro mejor modelo.
    • La línea de regresión lineal es nuestro modelo base, y es el mejor modelo en este ejemplo.
  • Para 50 neuronas en la capa oculta (en el panel central), el MLP replica la línea de regresión lineal, es decir ha sido capaz de construir un modelo tan bueno como el mejor.

  • Para 100 neuronas (en el panel derecho), el MLP tiene un error MSE, más pequeño que la regresión lineal, sin embargo es el peor de los modelos MLP conseguidos.

    • Este es un ejemplo de sobreajuste. El modelo funciona bien para los datos observados, pero no es capaz de generalizar.
    • En el MLP obsevamos que a medida que la alura ($X$) baja, el peso ($y$) sube. Esto no puede ser correcto.

drawing

In [3]:
# Datos
x = np.array([63, 64, 66, 69, 69, 71, 71, 72, 73, 75])  # altura (pulgadas)

y = np.array([127, 121, 142, 157, 162, 156, 169, 165, 181, 208])  # peso (libras)

@ipywidgets.interact
def plot(N_h = [5, 10, 50, 100, 500, 1000],
         zoom=False,
        ):
    mlpreg = MLPRegressor(activation='relu',max_iter=10000,hidden_layer_sizes=(N_h,))
    x_scaled = (x - x.min()) / (x.max() - x.min())  # min-max normalization
    mlpreg.fit(x_scaled.reshape(-1,1),y)
    x_test = np.linspace(50, 100)
    x_test_scaled = (x_test - x.min()) / (x.max() - x.min())
    y_mlp = mlpreg.predict(x_test_scaled.reshape(-1, 1))
    plt.scatter(x, y, label='Data')
    plt.plot(x_test, y_mlp, '--r', label='MLP')
    # Instance model
    lin_reg = LinearRegression()
    # Fit model (train)
    lin_reg.fit(x.reshape(-1,1), y)  # our lineal model (base model)
    y_lr_test = lin_reg.coef_*x_test + lin_reg.intercept_  # linear model
    plt.plot(x_test, y_lr_test, ':g', label='LR')
    l=plt.legend()
    if zoom:
        plt.xlim(x.min(), x.max())
interactive(children=(Dropdown(description='N_h', options=(5, 10, 50, 100, 500, 1000), value=5), Checkbox(valu…

Ejemplo: MNIST handwritten digits¶

MNIST (Modified National Institute of Standards and Technology database) es un conjunto de datos ampliamente utilizado en el campo del aprendizaje automático y la visión por computadora. Contiene un conjunto de imágenes en escala de grises de dígitos escritos a mano, cada uno en un formato de 28x28=784 píxeles. Estas imágenes están etiquetadas con los dígitos correspondientes del 0 al 9.

El conjunto de datos MNIST es utilizado principalmente como un punto de referencia para probar y comparar algoritmos y modelos de clasificación de imágenes. Es un conjunto de datos estándar en la comunidad de aprendizaje automático y se ha utilizado en una amplia variedad de aplicaciones y experimentos.

Debido a su simplicidad y tamaño manejable, MNIST se ha convertido en un punto de partida común para aquellos que desean aprender sobre el aprendizaje automático y comenzar a trabajar con modelos de redes neuronales.

El MNIST digits es esencialmente un subconjunto de MNIST, centrado específicamente en los dígitos escritos a mano. Contiene 60,000 imágenes de entrenamiento y 10,000 imágenes de prueba, cada una etiquetada con el dígito que representa.

El MNIST digits es un problema de clasificación de imágenes. El objetivo es clasificar imágenes en escala de grises de dígitos escritos a mano en sus respectivas categorías numéricas, que van del 0 al 9. Cada imagen en el conjunto de datos MNIST es un cuadrado de 28x28 píxeles, lo que resulta en un total de 784 píxeles por imagen. Cada píxel tiene un valor de intensidad de 0 a 255, donde 0 representa blanco y 255 representa negro.

El desafío consiste en entrenar un modelo de aprendizaje automático o una red neuronal que pueda tomar una imagen de un dígito escrito a mano como entrada y predecir correctamente el dígito que representa. Esencialmente, el modelo debe aprender a reconocer y diferenciar entre diferentes formas de dígitos escritos a mano, independientemente de su estilo de escritura o variaciones en la orientación y el tamaño. Es un problema de clasificación, aunque puede tratarse como uno de regresión.

Este problema es un problema clásico en el campo del aprendizaje automático y la visión por computadora, y se utiliza comúnmente como un punto de referencia para probar y comparar algoritmos y modelos de clasificación de imágenes. La precisión de los modelos en el conjunto de datos MNIST es a menudo considerada como un indicador de su capacidad para generalizar y manejar problemas de clasificación de imágenes más complejos en la vida real.

 Visualización del dataset¶

In [4]:
mnist_digits = pd.read_csv('https://raw.githubusercontent.com/sbussmann/kaggle-mnist/master/Data/train.csv')
mnist_digits.head()
Out[4]:
label pixel0 pixel1 pixel2 pixel3 pixel4 pixel5 pixel6 pixel7 pixel8 ... pixel774 pixel775 pixel776 pixel777 pixel778 pixel779 pixel780 pixel781 pixel782 pixel783
0 1 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
1 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
2 1 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
3 4 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
4 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0

5 rows × 785 columns

In [5]:
plt.figure(figsize=(8,8))

sample_size = 10

for value in range(10):
    digit_ = mnist_digits.query('label == '+str(value)).head(sample_size)
    digit_.drop(columns=['label'], inplace=True)
    for i, obs_i in enumerate(digit_.iterrows()):
        plt.subplot(10, 10, (value*sample_size)+i+1)
        sns.heatmap(np.array(obs_i[1]).reshape(28,28), cbar=False)
        plt.axis('off')
plt.show()
In [6]:
ax = sns.countplot(data=mnist_digits, x="label")
ax.bar_label(ax.containers[0]);
In [7]:
digit = np.array(mnist_digits.iloc[1][1:])
plt.figure(figsize=(12,12))
sns.heatmap(digit.reshape(28,28), annot=True, fmt="d", cbar=False, square=True)
plt.show()
In [8]:
digit[np.where(digit)[0]] = 1
plt.figure(figsize=(12,12))
sns.heatmap(digit.reshape(28,28), annot=True, fmt="d", cmap="gray", cbar=False, square=True)
plt.show()

Revisa: https://docs.opencv.org/4.x/d9/d61/tutorial_py_morphological_ops.html

In [9]:
binary_digits = mnist_digits.copy()
binary_digits.iloc[:,1:] = (binary_digits.iloc[:,1:]).where(binary_digits.iloc[:,1:] == 0, 1)
binary_digits.head()
Out[9]:
label pixel0 pixel1 pixel2 pixel3 pixel4 pixel5 pixel6 pixel7 pixel8 ... pixel774 pixel775 pixel776 pixel777 pixel778 pixel779 pixel780 pixel781 pixel782 pixel783
0 1 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
1 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
2 1 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
3 4 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
4 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0

5 rows × 785 columns

In [10]:
sns.heatmap(np.array(binary_digits.iloc[1,1:]).reshape(28,28), square=True, cbar=False)
Out[10]:
<Axes: >

Escalado de datos¶

Sí, es una buena práctica escalar los datos antes de alimentarlos a un perceptrón multicapa u otro tipo de modelo de redes neuronales. El escalado de datos puede ayudar a mejorar el rendimiento y la convergencia del modelo.

Se listan a continuación algunas razones por las cuales es recomendable escalar los datos:

  1. Mayor estabilidad numérica: Al escalar los datos, reduces la varianza de las características, lo que puede ayudar a evitar problemas numéricos durante el entrenamiento del modelo.

  2. Acelera la convergencia: El escalado de datos puede ayudar al algoritmo de optimización a converger más rápidamente hacia la solución óptima, ya que las características están en un rango similar.

  3. Iguala la importancia de las características: Al escalar las características, evitas que las características con valores más grandes dominen las que tienen valores más pequeños, lo que puede garantizar que todas las características contribuyan de manera equitativa al aprendizaje del modelo.

  4. Mejora el rendimiento: En general, los modelos de redes neuronales tienden a funcionar mejor cuando los datos están escalados, lo que puede llevar a una mejor generalización y rendimiento del modelo en datos no vistos.

Algunas técnicas comunes de escalado de datos incluyen la normalización y la estandarización:

  • Normalización: Escala los datos para que estén en el rango de 0 a 1. Esto se hace restando el valor mínimo y dividiendo por la diferencia entre el valor máximo y el mínimo de cada característica.

  • Estandarización: Transforma los datos para que tengan una media de 0 y una desviación estándar de 1. Esto se logra restando la media y dividiendo por la desviación estándar de cada característica.

Escalar los datos antes de alimentarlos a un perceptrón multicapa puede mejorar su rendimiento y estabilidad, y es una práctica comúnmente recomendada en el campo del aprendizaje automático.

Tanto la normalización como la estandarización son técnicas comunes para escalar datos antes de alimentarlos a modelos de aprendizaje automático, como perceptrones multicapa y otros tipos de redes neuronales. Sin embargo, la elección entre normalización y estandarización depende del contexto del problema y las características de los datos. A continuación se listan algunas pautas generales sobre cuándo usar cada técnica:

  1. Normalización (min-max scaler*):

    • Se utiliza cuando los datos tienen distribuciones sesgadas o no están distribuidos normalmente.
    • Es útil cuando se necesita que los datos estén en un rango específico, como [0, 1].
    • Se recomienda cuando la escala absoluta de las características no es relevante para el modelo, pero se desea conservar la relación relativa entre las características.
  2. Estandarización:

    • Se utiliza cuando se asume que los datos siguen una distribución normal o aproximadamente normal.
    • Es útil cuando se desea eliminar la media y la escala de las características, lo que puede facilitar la interpretación de los coeficientes del modelo.
    • Se recomienda cuando la magnitud absoluta de las características es importante para el modelo, pero no se necesita un rango específico para los datos.

La normalización es más apropiada cuando se desea conservar la relación relativa entre las características y se necesita un rango específico para los datos, mientras que la estandarización es más apropiada cuando se desea eliminar la media y la escala de las características y se asume que los datos siguen una distribución normal. En muchos casos, ambas técnicas pueden dar resultados similares, por lo que puede ser útil probar ambas y evaluar cómo afectan al rendimiento del modelo.

In [11]:
data_scaled = mnist_digits[mnist_digits.columns[:-1]]  # leave label out

data_scaled = data_scaled / 255  # Divide over the maximum, equivalente to min-max scaler

ds = pd.DataFrame(data_scaled, columns=mnist_digits.columns[:-1])
ds["label"] = mnist_digits.label

Visualización usando t-SNE¶

In [12]:
%%time
from sklearn.manifold import TSNE

plt.figure(figsize=(15,15))
p = 30
tsne = TSNE(n_components = 2, perplexity = p, random_state=0)
tsne_results = tsne.fit_transform(ds.drop(columns="label"))

tsne_results=pd.DataFrame(tsne_results, columns=['C1', 'C2'])

tsne_results['digit'] = pd.Categorical(ds.label)

sns.scatterplot(data=tsne_results, x='C1', y='C2',
                hue="digit", s=4, alpha=0.5)
plt.title('Perplexity = '+ str(p))
plt.show()
CPU times: user 19min 25s, sys: 5.19 s, total: 19min 30s
Wall time: 9min 58s
In [13]:
%%time
from sklearn.neural_network import MLPClassifier

mlp = MLPClassifier(hidden_layer_sizes=(64,), max_iter=500)

X_scaled, y = ds.drop(columns="label"), ds.label

mlp.fit(X_scaled, y)
CPU times: user 2min 29s, sys: 2.89 s, total: 2min 32s
Wall time: 1min 16s
Out[13]:
MLPClassifier(hidden_layer_sizes=(64,), max_iter=500)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
MLPClassifier(hidden_layer_sizes=(64,), max_iter=500)
In [14]:
y_pred = mlp.predict(X_scaled)

labels = np.unique(y)

cm = pd.DataFrame(confusion_matrix(y, y_pred, labels=labels),
                  columns=labels,
                  index=labels)

sns.heatmap(cm, square=True, annot=True, fmt='d', cbar=False)
plt.xlabel('True label')
plt.ylabel('Predicted label')

print(classification_report(y, y_pred))
              precision    recall  f1-score   support

           0       1.00      1.00      1.00      4132
           1       1.00      1.00      1.00      4684
           2       1.00      1.00      1.00      4177
           3       0.99      1.00      1.00      4351
           4       1.00      1.00      1.00      4072
           5       1.00      1.00      1.00      3795
           6       1.00      1.00      1.00      4137
           7       1.00      1.00      1.00      4401
           8       1.00      1.00      1.00      4063
           9       1.00      1.00      1.00      4188

    accuracy                           1.00     42000
   macro avg       1.00      1.00      1.00     42000
weighted avg       1.00      1.00      1.00     42000

In [15]:
%%time
X_train, X_test, y_train, y_test = train_test_split(X_scaled, y, test_size=0.3, stratify=y)

mlp.fit(X_train, y_train)

y_pred = mlp.predict(X_test)

labels = np.unique(y)

cm = pd.DataFrame(confusion_matrix(y_test, y_pred, labels=labels),
                  columns=labels,
                  index=labels)

sns.heatmap(cm, square=True, annot=True, fmt='d', cbar=False)
plt.xlabel('True label')
plt.ylabel('Predicted label')

print(classification_report(y_test, y_pred))
              precision    recall  f1-score   support

           0       0.98      0.98      0.98      1240
           1       0.98      0.99      0.98      1405
           2       0.97      0.97      0.97      1253
           3       0.96      0.96      0.96      1305
           4       0.96      0.97      0.97      1222
           5       0.96      0.96      0.96      1139
           6       0.97      0.98      0.98      1241
           7       0.97      0.97      0.97      1320
           8       0.96      0.94      0.95      1219
           9       0.95      0.95      0.95      1256

    accuracy                           0.97     12600
   macro avg       0.97      0.97      0.97     12600
weighted avg       0.97      0.97      0.97     12600

CPU times: user 1min 34s, sys: 1.33 s, total: 1min 35s
Wall time: 48 s
In [16]:
%%time
# Using KFold cross validation
from sklearn.model_selection import KFold

# This is 10-fold cross validation
cv_method = KFold(n_splits=10)  # change n_splits for your dataset

# This is the final model you want to deploy
from sklearn.model_selection import cross_val_score

mlp = MLPClassifier(hidden_layer_sizes=(64,), max_iter=1000)

scores = cross_val_score(mlp, X_scaled, y, cv=cv_method)
scores
CPU times: user 26min 48s, sys: 33.2 s, total: 27min 21s
Wall time: 13min 53s
Out[16]:
array([0.97357143, 0.97285714, 0.97428571, 0.97190476, 0.96738095,
       0.96666667, 0.96595238, 0.975     , 0.97095238, 0.97071429])
In [17]:
sns.boxplot(y=scores)
Out[17]:
<Axes: >

 Ejercicio¶

  • Realice la optimización de hiper-parámetros para el perceptrón multicapa.
    • Vea: https://nbviewer.org/github/marsgr6/ml-online/blob/main/ensemble_models.ipynb

 MLP para un problema de regresión¶

El conjunto de datos "Auto MPG" contiene información sobre diferentes modelos de automóviles de varios fabricantes y sus respectivas características técnicas. Fue utilizado por primera vez en el artículo "Auto Fuel Efficiency Data" de Ross Quinlan en 1987.

Este conjunto de datos incluye varias características del automóvil, como el cilindraje, la cilindrada, el peso, la potencia y el año del modelo, entre otros. El objetivo principal de este conjunto de datos es predecir la eficiencia del combustible en términos de millas por galón (MPG) de un automóvil en función de sus características.

Descripción de las columnas que se encuentran en este conjunto de datos:

  1. MPG: Millas por galón, que es la variable objetivo a predecir.
  2. Cilindros: Número de cilindros en el motor.
  3. Desplazamiento: Desplazamiento del motor en pulgadas cúbicas.
  4. Potencia neta: Potencia neta del motor en caballos de fuerza (HP).
  5. Peso: Peso del automóvil en libras.
  6. Aceleración: Tiempo necesario para que el automóvil alcance una velocidad de 60 millas por hora (mph) en segundos.
  7. Modelo del año: Año de fabricación del automóvil.
  8. Origen: Código numérico que representa el origen del automóvil (1: Americano, 2: Europeo, 3: Japonés).
  9. Nombre del automóvil: Marca y modelo del automóvil.

El objetivo típico de análisis o modelado con este conjunto de datos es predecir el rendimiento en términos de millas por galón (MPG) de un automóvil en función de sus características técnicas, lo que puede ser útil para comprender cómo diferentes variables impactan en la eficiencia del combustible y para tomar decisiones informadas sobre la compra o el diseño de automóviles más eficientes en términos de consumo de combustible.

In [18]:
mpg_data = pd.read_csv("https://raw.githubusercontent.com/marsgr6/ml-online/main/data/auto-mpg.csv")
mpg_data.head()
Out[18]:
mpg cylinders displacement horsepower weight acceleration model year origin car name
0 18.0 8 307.0 130.0 3504 12.0 70 1 chevrolet chevelle malibu
1 15.0 8 350.0 165.0 3693 11.5 70 1 buick skylark 320
2 18.0 8 318.0 150.0 3436 11.0 70 1 plymouth satellite
3 16.0 8 304.0 150.0 3433 12.0 70 1 amc rebel sst
4 17.0 8 302.0 140.0 3449 10.5 70 1 ford torino
In [19]:
mpg_data.isnull().sum()
Out[19]:
mpg             0
cylinders       0
displacement    0
horsepower      6
weight          0
acceleration    0
model year      0
origin          0
car name        0
dtype: int64
In [20]:
data_selected = mpg_data.drop(columns="car name")  # leave car name out
data_selected['origin'] = data_selected['origin'].map({1: 'USA', 2: 'Europe', 3: 'Japan'})

X, y = data_selected.drop(columns=["mpg", "origin"]), data_selected.mpg

scaler = StandardScaler()
scaler.fit(X)

X_scaled = pd.DataFrame(scaler.transform(X), columns=X.columns)

data_selected.head()
Out[20]:
mpg cylinders displacement horsepower weight acceleration model year origin
0 18.0 8 307.0 130.0 3504 12.0 70 USA
1 15.0 8 350.0 165.0 3693 11.5 70 USA
2 18.0 8 318.0 150.0 3436 11.0 70 USA
3 16.0 8 304.0 150.0 3433 12.0 70 USA
4 17.0 8 302.0 140.0 3449 10.5 70 USA
In [21]:
data_selected.describe()
Out[21]:
mpg cylinders displacement horsepower weight acceleration model year
count 398.000000 398.000000 398.000000 392.000000 398.000000 398.000000 398.000000
mean 23.514573 5.454774 193.425879 104.469388 2970.424623 15.568090 76.010050
std 7.815984 1.701004 104.269838 38.491160 846.841774 2.757689 3.697627
min 9.000000 3.000000 68.000000 46.000000 1613.000000 8.000000 70.000000
25% 17.500000 4.000000 104.250000 75.000000 2223.750000 13.825000 73.000000
50% 23.000000 4.000000 148.500000 93.500000 2803.500000 15.500000 76.000000
75% 29.000000 8.000000 262.000000 126.000000 3608.000000 17.175000 79.000000
max 46.600000 8.000000 455.000000 230.000000 5140.000000 24.800000 82.000000

Pairplot¶

  • Aunque no se trata de un problema de clasificación, estamos usando origen como hue.
    • Ejercicio: Construya un modelo para predecir el origen del vehículo.
      • Son las clases balanceadas.
      • Cual es el rendimiento de su modelo.
In [22]:
data_selected.origin = pd.Categorical(data_selected.origin)
sns.pairplot(data=data_selected, hue="origin")
Out[22]:
<seaborn.axisgrid.PairGrid at 0x7f8c9dba5510>

Multicolinealidad*¶

En la regresión lineal, la multicolinealidad puede plantear desafíos significativos debido a la forma en que se ajusta el modelo, especialmente al intentar invertir la matriz (X^TX) para estimar los parámetros. La multicolinealidad perfecta hace que la matriz no sea invertible, mientras que la multicolinealidad no perfecta puede llevar a estimaciones de parámetros inexactas o inestables debido al alto número de condición de la matriz.

En contraste, las redes neuronales, incluidas las perceptrones multicapa (MLP), no enfrentan los mismos problemas con la multicolinealidad. Esto se debe a que las redes neuronales generalmente se entrenan utilizando técnicas como la retropropagación, que no requieren invertir matrices o asumir una solución única al problema. Además, las redes neuronales pueden manejar variables de entrada altamente correlacionadas, como se observa en tareas como la clasificación de imágenes, donde las características pueden mostrar fuertes interdependencias.

La flexibilidad inherente de las redes neuronales, combinada con su capacidad para aprender mapeos complejos de entrada a salida, les permite manejar eficazmente la multicolinealidad sin los mismos efectos adversos observados en la regresión lineal. Sin embargo, es importante tener en cuenta que si bien la multicolinealidad puede no ser una preocupación directa en las redes neuronales, otros problemas como el sobreajuste y la desaparición/explotación de gradientes aún pueden afectar el rendimiento y la estabilidad del modelo. Por lo tanto, un diseño cuidadoso del modelo y procedimientos de entrenamiento son cruciales para lograr resultados óptimos en aplicaciones de redes neuronales.

In [23]:
sns.heatmap(data_selected.drop(columns="origin").corr(), annot=True)
Out[23]:
<Axes: >
In [24]:
X_scaled.head()
Out[24]:
cylinders displacement horsepower weight acceleration model year
0 1.498191 1.090604 0.664133 0.630870 -1.295498 -1.627426
1 1.498191 1.503514 1.574594 0.854333 -1.477038 -1.627426
2 1.498191 1.196232 1.184397 0.550470 -1.658577 -1.627426
3 1.498191 1.061796 1.184397 0.546923 -1.295498 -1.627426
4 1.498191 1.042591 0.924265 0.565841 -1.840117 -1.627426
In [25]:
X_scaled.isnull().sum()
Out[25]:
cylinders       0
displacement    0
horsepower      6
weight          0
acceleration    0
model year      0
dtype: int64
In [26]:
# create an object for KNNImputer
imputer = KNNImputer(n_neighbors=2)
X_scaled = pd.DataFrame(imputer.fit_transform(X_scaled), columns=X.columns)

X_scaled.isnull().sum()
Out[26]:
cylinders       0
displacement    0
horsepower      0
weight          0
acceleration    0
model year      0
dtype: int64

KFold cross validation¶

In [27]:
%%time
# Using KFold cross validation
from sklearn.model_selection import KFold
# This is the final model you want to deploy
from sklearn.model_selection import cross_val_score

# This is 10-fold cross validation
cv_method = KFold(n_splits=10)  # change n_splits for your dataset

mlp = MLPRegressor(hidden_layer_sizes=(512,), max_iter=500)

scores = cross_val_score(mlp, X_scaled, y, cv=cv_method, scoring="r2")
scores
CPU times: user 33.2 s, sys: 448 ms, total: 33.6 s
Wall time: 16.9 s
Out[27]:
array([0.85934032, 0.871978  , 0.73380954, 0.86680045, 0.73904889,
       0.92716591, 0.838334  , 0.84642855, 0.31551456, 0.50828224])
In [28]:
sns.boxplot(y=scores)
Out[28]:
<Axes: >
In [29]:
%%time
mlp = MLPRegressor(hidden_layer_sizes=(512,), max_iter=500)

mlp.fit(X_scaled, y)

y_pred = mlp.predict(X_scaled)

mpg_data["mpg_mlp"] = y_pred
CPU times: user 3.54 s, sys: 52 ms, total: 3.6 s
Wall time: 1.8 s

Evaluación de un modelo de regresión¶

Estas son algunas métricas comunes utilizadas para evaluar la calidad de los modelos de regresión:

  1. Error Cuadrático Medio (MSE):

    • El Error Cuadrático Medio es la media de los errores al cuadrado entre las predicciones del modelo y los valores reales.
    • Fórmula: $$MSE = \frac{1}{n} \sum_{i=1}^{n} (y_i - \hat{y}_i)^2$$
    • Donde $n$ es el número de muestras, $y_i$ son los valores reales, y $\hat{y}_i$ son las predicciones del modelo para cada muestra.
  2. Raíz del Error Cuadrático Medio (RMSE):

    • El RMSE es simplemente la raíz cuadrada del MSE, lo que nos da una medida de error en la misma escala que la variable de respuesta original.
    • Fórmula: $$RMSE = \sqrt{MSE}$$
  3. Error Absoluto Medio (MAE):

    • El Error Absoluto Medio es la media de las diferencias absolutas entre las predicciones del modelo y los valores reales.
    • Fórmula: $$MAE = \frac{1}{n} \sum_{i=1}^{n} |y_i - \hat{y}_i|$$
  4. Error Porcentual Absoluto Medio (MAPE):

    • El MAPE es una métrica de error relativo expresada en porcentaje. Mide el promedio de los errores absolutos como una proporción de los valores reales.
    • Fórmula: $$MAPE = \frac{100}{n} \sum_{i=1}^{n} \left|\frac{y_i - \hat{y}_i}{y_i}\right|$$
  5. Coeficiente de Determinación (R²):

    • El coeficiente de determinación, también conocido como R cuadrado, es una medida de la proporción de la varianza en la variable de respuesta que es predecible a partir de las características del modelo.
    • Fórmula: $$R^2 = 1 - \frac{\sum_{i=1}^{n} (y_i - \hat{y}_i)^2}{\sum_{i=1}^{n} (y_i - \bar{y})^2}$$
    • Donde $\bar{y}$ es la media de los valores reales $y_i$.

Estas métricas son importantes para evaluar la precisión y el rendimiento de los modelos de regresión. Dependiendo del contexto y la naturaleza del problema, se pueden utilizar diferentes métricas para obtener una comprensión completa del rendimiento del modelo.

In [30]:
from sklearn.metrics import mean_squared_error
from sklearn.metrics import mean_absolute_percentage_error
from sklearn.metrics import r2_score
from sklearn.metrics import mean_absolute_error

def plot_evaluation(y, y_pred):
    fig = plt.figure(constrained_layout=True, figsize=(12,6))
    gs = fig.add_gridspec(3, 3)
    ax1 = fig.add_subplot(gs[0, 0])
    ax2 = fig.add_subplot(gs[0, 1:])
    ax3 = fig.add_subplot(gs[1, 0:])
    ax4 = fig.add_subplot(gs[2, 0:])


    sns.scatterplot(x=y, y=y_pred, ax=ax1)
    ax1.plot(mpg_data.mpg, mpg_data.mpg, 'r')  # line x = y

    ax2.plot(y, '.:', c="orange", label="Observed")
    ax2.plot(y_pred, '.:', c="cornflowerblue", label="Modeled")
    ax2.legend()

    ax3.plot(y, '.:', c="orange", label="Observed")
    ax3.plot(y_pred, '.:', c="cornflowerblue", label="Modeled")
    ax3.set_xlim(0, 200)
    ax3.legend()

    ax4.plot(y, '.:', c="orange", label="Observed")
    ax4.plot(y_pred, '.:', c="cornflowerblue", label="Modeled")
    ax4.set_xlim(200, 400)
    ax4.legend()

    print("MSE:", mean_squared_error(y, y_pred))
    print("RMSE:", np.sqrt(mean_squared_error(y, y_pred)))
    print("MAE:", mean_absolute_error(y, y_pred))
    print("MAPE:", mean_absolute_percentage_error(y, y_pred))
    print("R2:", r2_score(y, y_pred))

plot_evaluation(y, y_pred)
MSE: 6.6717879117144365
RMSE: 2.5829804319263507
MAE: 1.84982659245948
MAPE: 0.07885705635715959
R2: 0.8905117688907633

Evaluar una regresión de forma visual es una práctica común para comprender cómo se ajusta el modelo a los datos y para identificar posibles problemas como el sobreajuste o subajuste. Algunas técnicas comunes para evaluar una regresión de forma visual son:

  1. Gráfico de dispersión con línea de regresión: Un gráfico de dispersión de los datos reales con una línea que representa la regresión ajustada proporciona una visualización básica del ajuste del modelo a los datos. Puedes ver cómo la línea se ajusta a los puntos de datos y evaluar visualmente si captura la tendencia general de los datos.

  2. Gráfico de residuos: Un gráfico de residuos muestra la diferencia entre los valores reales y las predicciones del modelo (los residuos) en función de las características o la variable de respuesta. Si el modelo es adecuado, los residuos deberían distribuirse aleatoriamente alrededor de cero y no deberían mostrar patrones discernibles.

  3. Diagrama de Q-Q (quantile-quantile): Un diagrama de Q-Q compara los cuantiles de una distribución teórica (como la distribución normal) con los cuantiles de una distribución de residuos. Si los residuos siguen una distribución normal, los puntos en el diagrama de Q-Q deben seguir aproximadamente una línea diagonal.

  4. Gráfico de aprendizaje (learning curve): Un gráfico de aprendizaje muestra cómo el error de entrenamiento y el error de validación cambian a medida que varía el tamaño del conjunto de entrenamiento. Esto puede ayudar a identificar si el modelo está sobreajustando o subajustando los datos y si se beneficiaría de más datos de entrenamiento.

  5. Gráficos de influencia: Estos gráficos muestran cómo cada punto de datos individual influye en los resultados de la regresión. Pueden ayudar a identificar puntos de datos atípicos o influentes que podrían estar afectando el modelo.

  6. Gráficos de predicción vs. observado: Estos gráficos comparan las predicciones del modelo con los valores reales en un conjunto de datos de prueba o validación. Si el modelo es bueno, las predicciones deben seguir una línea diagonal de pendiente 1.

Estas son solo algunas técnicas que puedes utilizar para evaluar una regresión de forma visual. La elección de las técnicas dependerá del modelo específico y del tipo de datos que estés analizando. Utilizar una combinación de estas visualizaciones puede proporcionar una comprensión más completa del rendimiento del modelo de regresión.

  • Ejercicio:
    • Qué gráficos hemos usado arriba.
    • Construya gráficos adicionales (cuáles).

Deep Neural Network (DNN)¶

Una Deep Neural Network (DNN) feedforward es un tipo específico de red neuronal artificial (ANN) donde la información fluye en una sola dirección, desde la capa de entrada hacia la capa de salida, sin conexiones hacia atrás o ciclos en la estructura de la red. En este tipo de arquitectura, cada capa de neuronas está completamente conectada a la capa siguiente, lo que significa que cada neurona en una capa envía señales a todas las neuronas de la capa siguiente.

Una DNN feedforward típicamente consta de múltiples capas ocultas entre la capa de entrada y la capa de salida, lo que permite al modelo aprender representaciones jerárquicas y complejas de los datos de entrada. Cada neurona en una capa oculta realiza una combinación lineal de las salidas de todas las neuronas en la capa anterior, seguida de la aplicación de una función de activación no lineal. Estas funciones de activación introducen no linealidades en el modelo, permitiendo la captura de patrones y relaciones no lineales en los datos.

Una diferencia importante entre una DNN feedforward y un Multi-Layer Perceptron (MLP) es que el término "MLP" se utiliza a menudo de manera intercambiable con "DNN feedforward". Ambos términos se refieren a redes neuronales con múltiples capas ocultas donde la información fluye en una sola dirección. Sin embargo, es importante destacar que "MLP" a menudo se usa para referirse a arquitecturas de redes neuronales más simples, especialmente cuando se usan solo una o dos capas ocultas. Por otro lado, "DNN" se refiere generalmente a arquitecturas de redes neuronales más profundas con varias capas ocultas.

En resumen, una DNN feedforward es un tipo de red neuronal donde la información fluye en una sola dirección, desde la capa de entrada hacia la capa de salida, y puede tener múltiples capas ocultas para aprender representaciones complejas de los datos. El término "MLP" a menudo se usa de manera intercambiable con "DNN feedforward", pero a veces se refiere a arquitecturas de redes neuronales más simples con menos capas ocultas.

In [31]:
import tensorflow as tf

from tensorflow import keras
from tensorflow.keras import layers
#from tensorflow.keras.layers.experimental import preprocessing
2024-02-15 18:59:31.252266: I tensorflow/core/platform/cpu_feature_guard.cc:182] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations.
To enable the following instructions: SSE4.1 SSE4.2 AVX AVX2 FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags.
In [32]:
data = pd.DataFrame(scaler.inverse_transform(X_scaled), columns=X_scaled.columns)
data["mpg"] = y
data.head()
Out[32]:
cylinders displacement horsepower weight acceleration model year mpg
0 8.0 307.0 130.0 3504.0 12.0 70.0 18.0
1 8.0 350.0 165.0 3693.0 11.5 70.0 15.0
2 8.0 318.0 150.0 3436.0 11.0 70.0 18.0
3 8.0 304.0 150.0 3433.0 12.0 70.0 16.0
4 8.0 302.0 140.0 3449.0 10.5 70.0 17.0
In [33]:
train_data = data.sample(frac=0.8, random_state=0)
test_data = data.drop(train_data.index)

train_data.shape, test_data.shape
Out[33]:
((318, 7), (80, 7))
In [34]:
train_features = train_data.copy()
test_features = test_data.copy()

train_target = train_features.pop('mpg')
test_target = test_features.pop('mpg')
In [35]:
normalizer = tf.keras.layers.Normalization(axis=-1)

normalizer.adapt(np.array(train_features))
2024-02-15 18:59:33.739417: I tensorflow/core/common_runtime/process_util.cc:146] Creating new thread pool with default inter op setting: 2. Tune using inter_op_parallelism_threads for best performance.
In [36]:
def build_and_compile_model(norm):
    model = keras.Sequential([
      norm,
      layers.Dense(256, activation='relu'),
      layers.Dense(256, activation='relu'),
      layers.Dense(1)
    ])

    model.compile(loss='mean_absolute_error',
                optimizer=tf.keras.optimizers.Adam(0.001))
    return model
In [37]:
dnn_model = build_and_compile_model(normalizer)
dnn_model.summary()
Model: "sequential"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 normalization (Normalizatio  (None, 6)                13        
 n)                                                              
                                                                 
 dense (Dense)               (None, 256)               1792      
                                                                 
 dense_1 (Dense)             (None, 256)               65792     
                                                                 
 dense_2 (Dense)             (None, 1)                 257       
                                                                 
=================================================================
Total params: 67,854
Trainable params: 67,841
Non-trainable params: 13
_________________________________________________________________
In [38]:
%%time
history = dnn_model.fit(
    train_features,
    train_target,
    validation_split=0.2,
    verbose=0, epochs=100)
CPU times: user 26.7 s, sys: 28.7 s, total: 55.5 s
Wall time: 21.4 s
In [39]:
def plot_loss(history):
    plt.plot(history.history['loss'], label='loss')
    plt.plot(history.history['val_loss'], label='val_loss')
    plt.ylim([0, 10])
    plt.xlabel('Epoch')
    plt.ylabel('Loss [MPG]')
    plt.legend()
    plt.grid(True)

plot_loss(history)

Gráfico de pérdida de entrenamiento¶

Un gráfico de pérdida de entrenamiento (train loss) versus pérdida de prueba (test loss) es una representación visual que muestra cómo cambian las pérdidas (errores) del modelo durante el entrenamiento y la evaluación en un conjunto de datos de prueba o validación.

  • Pérdida de entrenamiento (train loss): Es la métrica de error que se calcula utilizando los datos de entrenamiento durante el proceso de entrenamiento del modelo. Mide qué tan bien se ajusta el modelo a los datos de entrenamiento.

  • Pérdida de prueba (test loss): Es la métrica de error que se calcula utilizando un conjunto de datos independiente, no visto por el modelo durante el entrenamiento. Se utiliza para evaluar el rendimiento del modelo en datos no vistos y para estimar su capacidad de generalización.

En el gráfico, el eje x representa las iteraciones o épocas de entrenamiento, mientras que el eje y representa la pérdida (error). Las curvas de pérdida de entrenamiento y prueba se trazan en el mismo gráfico para comparar cómo se comporta el modelo durante el entrenamiento y la evaluación.

Idealmente, durante el entrenamiento del modelo, tanto la pérdida de entrenamiento como la pérdida de prueba disminuirán con el tiempo. Sin embargo, si el modelo comienza a sobreajustar los datos de entrenamiento, es probable que la pérdida de prueba aumente, mientras que la pérdida de entrenamiento continuará disminuyendo. Esto indica que el modelo está aprendiendo a ajustarse demasiado bien a los datos de entrenamiento y no puede generalizar bien a nuevos datos.

Un gráfico de pérdida de entrenamiento versus pérdida de prueba es una herramienta útil para monitorear el rendimiento del modelo durante el entrenamiento y la evaluación, y para identificar problemas de sobreajuste o subajuste. Si las curvas de pérdida de entrenamiento y prueba divergen significativamente, es posible que sea necesario ajustar la complejidad del modelo, aplicar técnicas de regularización o recopilar más datos para mejorar la generalización del modelo.

Función de pérdida¶

Una función de pérdida, o loss function en inglés, es una medida que cuantifica la discrepancia entre las predicciones de un modelo de aprendizaje automático y los valores reales del conjunto de datos de entrenamiento. Su importancia radica en varios aspectos clave del proceso de entrenamiento y evaluación de modelos en machine learning:

  1. Optimización del modelo: Durante el entrenamiento, el objetivo principal es minimizar la función de pérdida. Esto guía al modelo a ajustar sus parámetros de manera que las predicciones se acerquen lo más posible a los valores reales.

  2. Evaluación del rendimiento: La función de pérdida proporciona una medida cuantitativa del rendimiento del modelo. Cuanto menor sea la pérdida, mejor será el rendimiento del modelo en el conjunto de datos de entrenamiento.

  3. Comparación entre modelos: Permite comparar el rendimiento de diferentes modelos en un problema dado. Los modelos que minimizan la función de pérdida de manera más efectiva suelen producir predicciones más precisas.

  4. Regularización: Algunas funciones de pérdida incluyen términos de regularización que penalizan modelos más complejos, ayudando a prevenir el sobreajuste.

  5. Ajuste del modelo a objetivos específicos: Dependiendo del tipo de problema y los objetivos específicos, se pueden seleccionar diferentes funciones de pérdida para optimizar el modelo de manera acorde a dichos objetivos (por ejemplo, minimizar errores cuadráticos para regresión o entropía cruzada para clasificación).

La función de pérdida es un componente fundamental en el entrenamiento y evaluación de modelos de aprendizaje automático, ya que guía el proceso de optimización del modelo y proporciona una medida cuantitativa del rendimiento del mismo.

Aquí tienes las definiciones resumidas de las funciones de pérdida comunes:

Regresión:¶

  1. Error Cuadrático Medio (MSE - Mean Squared Error):

    • Calcula la media de los cuadrados de las diferencias entre las predicciones y los valores reales.
    • Fórmula: $$ \text{MSE} = \frac{1}{n} \sum_{i=1}^{n} (y_i - \hat{y}_i)^2 $$
  2. Error Absoluto Medio (MAE - Mean Absolute Error):

    • Calcula la media de las diferencias absolutas entre las predicciones y los valores reales.
    • Fórmula: $$ \text{MAE} = \frac{1}{n} \sum_{i=1}^{n} |y_i - \hat{y}_i| $$
  3. Error Cuadrático Medio Logarítmico (MSLE):

    • Similar al MSE pero calcula la media de los cuadrados de las diferencias logarítmicas.
    • Fórmula: $$ \text{MSLE} = \frac{1}{n} \sum_{i=1}^{n} (\log(y_i + 1) - \log(\hat{y}_i + 1))^2 $$
  4. Pérdida de Huber:

    • Similar al MAE pero con una transición suave entre MAE y MSE cerca de cero.

La pérdida de Huber es una función de pérdida que combina las ventajas del error cuadrático medio (MSE) y el error absoluto medio (MAE). Es especialmente útil en problemas de regresión donde pueden existir datos atípicos o ruido en los datos.

La fórmula de la pérdida de Huber para una sola muestra se puede expresar como:

$$ L(y, \hat{y}) = \begin{cases} \frac{1}{2}(y - \hat{y})^2, & \text{si } |y - \hat{y}| \leq \delta \\ \delta(|y - \hat{y}| - \frac{1}{2}\delta), & \text{en otro caso} \end{cases} $$

Donde:

  • $ y $ es el valor verdadero.
  • $ \hat{y} $ es la predicción del modelo.
  • $ \delta $ es un parámetro que controla el punto de transición entre el comportamiento lineal del error cuadrático y el comportamiento constante del error absoluto.

Cuando la diferencia absoluta entre el valor verdadero y la predicción es menor o igual que $ \delta $, la pérdida de Huber se comporta como el error cuadrático medio, lo que significa que penaliza los errores cuadráticos de manera similar al MSE. Sin embargo, cuando la diferencia absoluta supera $ \delta $, la pérdida de Huber cambia a un comportamiento lineal, similar al error absoluto medio. Esto permite que la pérdida de Huber sea menos sensible a los valores atípicos en los datos en comparación con el MSE.

En resumen, la pérdida de Huber es una alternativa robusta al MSE y al MAE, que puede ser útil en situaciones donde se desea una penalización más suave de los errores grandes.

Clasificación binaria:¶

  1. Entropía Cruzada Binaria (Binary Crossentropy):

    • Mide la discrepancia entre las distribuciones de probabilidad de las clases predichas y reales en problemas de clasificación binaria.
    • Fórmula: $$ \text{BCE} = -\frac{1}{n} \sum_{i=1}^{n} \left( y_i \cdot \log(\hat{y}_i) + (1 - y_i) \cdot \log(1 - \hat{y}_i) \right) $$
  2. Pérdida de Hinge:

    • Utilizado en máquinas de vectores de soporte (SVM) para clasificación binaria.

La pérdida de Hinge, también conocida como pérdida de bisagra, es una función de pérdida comúnmente utilizada en problemas de clasificación binaria y en máquinas de vectores de soporte (SVM). Esta función de pérdida es especialmente útil cuando se trabaja con clasificadores que generan una salida continua en lugar de probabilidades.

La fórmula de la pérdida de Hinge para una sola muestra se puede expresar como:

$$ L(y, \hat{y}) = \max(0, 1 - y \cdot \hat{y}) $$

Donde:

  • $ y $ es el valor verdadero de la clase (1 o -1 en el caso de clasificación binaria).
  • $ \hat{y} $ es la predicción del modelo (también puede ser una puntuación de decisión).
  • $ \max(0, 1 - y \cdot \hat{y}) $ representa el término de pérdida, que es cero si la predicción es correcta y positiva si la predicción es incorrecta.

La pérdida de Hinge penaliza las predicciones incorrectas con un valor proporcional a la distancia entre la predicción y el margen de decisión (1 en este caso). Si la predicción es correcta (es decir, $ y \cdot \hat{y} $ es mayor que 1), la pérdida es cero. Si la predicción está en el lado incorrecto del margen, la pérdida aumenta linealmente con la distancia al margen.

En resumen, el objetivo del entrenamiento es minimizar esta función de pérdida para mejorar la capacidad del modelo para clasificar correctamente las muestras.

  1. Pérdida de Sigmoid:
    • Similar a la entropía cruzada binaria pero con una función de activación sigmoide al final.
    • Fórmula: $$ \text{Sigmoid} = -\frac{1}{n} \sum_{i=1}^{n} \left( y_i \cdot \log(\hat{y}_i) + (1 - y_i) \cdot \log(1 - \hat{y}_i) \right) $$

Clasificación multiclase:¶

  1. Entropía Cruzada Categórica (Categorical Crossentropy):

    • Utilizada para problemas de clasificación con más de dos clases.
    • Fórmula: $$ \text{CCE} = -\frac{1}{n} \sum_{i=1}^{n} \sum_{j=1}^{C} y_{ij} \cdot \log(\hat{y}_{ij}) $$
  2. Pérdida Exponencial:

    • A menudo utilizada en clasificación multiclase, es similar a la pérdida softmax pero con una penalización más fuerte para las clasificaciones incorrectas.

La pérdida exponencial es una función de pérdida utilizada en problemas de clasificación multiclase. A menudo se emplea en conjunción con modelos de clasificación como redes neuronales. Esta función de pérdida es similar a la pérdida softmax, pero penaliza de manera más intensa las clasificaciones incorrectas.

La fórmula de la pérdida exponencial para una sola muestra se puede expresar como:

$$ L(y, \hat{y}) = - \sum_{i} y_i \cdot \exp(\hat{y}_i) $$

Donde:

  • $ y $ es un vector one-hot que representa las clases reales (etiquetas).
  • $ \hat{y} $ es un vector de probabilidades predichas por el modelo.
  • $ y_i $ y $ \hat{y}_i $ son las probabilidades reales y predichas para la clase $ i $, respectivamente.

Al igual que la pérdida softmax, la función de pérdida exponencial penaliza más fuertemente las clasificaciones incorrectas, pero utilizando la exponencial de la probabilidad predicha en lugar del logaritmo. El objetivo del entrenamiento es minimizar esta función de pérdida para mejorar la precisión de las predicciones del modelo.

  1. Pérdida Softmax:
    • Mide la discrepancia entre las distribuciones de probabilidad de las clases predichas y reales en problemas de clasificación multiclase.

La pérdida Softmax, también conocida como entropía cruzada categórica, se utiliza comúnmente en problemas de clasificación multiclase. Esta función de pérdida mide la discrepancia entre las distribuciones de probabilidad de las clases predichas y las clases reales.

La fórmula de la pérdida Softmax para una sola muestra se puede expresar como:

$$ L(y, \hat{y}) = - \sum_{i} y_i \cdot \log(\hat{y}_i) $$

Donde:

  • $ y $ es un vector one-hot que representa las clases reales (etiquetas).
  • $ \hat{y} $ es un vector de probabilidades predichas por el modelo.
  • $ y_i $ y $ \hat{y}_i $ son las probabilidades reales y predichas para la clase $ i $, respectivamente.

La función de pérdida Softmax penaliza más fuertemente las clasificaciones incorrectas, ya que la diferencia entre las probabilidades reales y las predichas se multiplica por el logaritmo de la probabilidad predicha. El objetivo del entrenamiento es minimizar esta función de pérdida para mejorar la precisión de las predicciones del modelo.

Clasificación multietiqueta:¶

  1. Pérdida de Hamming:
    • Mide la fracción de las etiquetas mal clasificadas.
    • Fórmula: $$ \text{Hamming} = \frac{1}{n} \sum_{i=1}^{n} \frac{1}{C} \sum_{j=1}^{C} I(y_{ij} \neq \hat{y}_{ij}) $$

Modelos generativos:¶

En el contexto de modelos generativos, aquí están las definiciones de las funciones de pérdida mencionadas:

  1. Divergencia de Kullback-Leibler (KL Divergence):

    • Mide la diferencia entre dos distribuciones de probabilidad. En el contexto de GANs, se utiliza para cuantificar cuán diferente es la distribución de las muestras generadas por el generador de la distribución real de los datos.
    • Fórmula: $$ D_{KL}(P \| Q) = \sum_{i} P(i) \log\left(\frac{P(i)}{Q(i)}\right) $$
  2. Pérdida del Autoencoder:

    • En un autoencoder, la pérdida se refiere a la discrepancia entre la entrada original y la salida reconstruida por el modelo. Se puede utilizar MSE (Error Cuadrático Medio) o MAE (Error Absoluto Medio) entre la entrada y la salida para entrenar el modelo.
    • Fórmula (MSE): $$ \text{MSE} = \frac{1}{n} \sum_{i=1}^{n} (x_i - \hat{x}_i)^2 $$
    • Fórmula (MAE): $$ \text{MAE} = \frac{1}{n} \sum_{i=1}^{n} |x_i - \hat{x}_i| $$

Estas funciones de pérdida se utilizan en el proceso de entrenamiento de modelos generativos para optimizar los parámetros del modelo y mejorar su capacidad para generar muestras realistas o reconstruir adecuadamente las entradas originales.

In [40]:
test_predictions = dnn_model.predict(test_features).flatten()

test_predictions = pd.Series(test_predictions, index=test_features.index)

plot_evaluation(test_target, test_predictions)
3/3 [==============================] - 0s 15ms/step
MSE: 6.9630055221161005
RMSE: 2.6387507502824326
MAE: 1.9365879344940187
MAPE: 0.08398161471412509
R2: 0.8731464086428874

CV evaluation¶

La evaluación de validación cruzada (CV, del inglés Cross-Validation) es una técnica utilizada en aprendizaje automático para evaluar el rendimiento de un modelo estadístico. El propósito principal de la validación cruzada es estimar la capacidad de generalización del modelo a datos no vistos.

En la validación cruzada, el conjunto de datos se divide en k subconjuntos más pequeños, llamados pliegues (folds). El modelo se entrena k veces, cada vez utilizando k-1 pliegues como datos de entrenamiento y uno de los pliegues restantes como datos de prueba. Luego, se calcula la métrica de evaluación (como la precisión, el error, el área bajo la curva ROC, etc.) en cada iteración y se promedian para obtener una estimación final del rendimiento del modelo.

Los tipos comunes de validación cruzada incluyen:

  1. Validación cruzada de k-fold (k-fold cross-validation): El conjunto de datos se divide en k pliegues de igual tamaño. El modelo se entrena k veces, cada vez utilizando k-1 pliegues como datos de entrenamiento y uno de los pliegues restantes como datos de prueba.

  2. Validación cruzada de leave-one-out (LOOCV): Es una variante de la validación cruzada de k-fold donde k es igual al número total de muestras en el conjunto de datos. En cada iteración, una única muestra se utiliza como conjunto de prueba y todas las demás se utilizan como conjunto de entrenamiento. Útil para datasets pequeños (very small data).

  3. Validación cruzada estratificada: Es una variante de la validación cruzada k-fold donde se asegura que cada clase esté representada proporcionalmente en cada pliegue. Esto es particularmente útil en problemas de clasificación desequilibrados.

La validación cruzada es una técnica robusta y ampliamente utilizada para evaluar modelos de aprendizaje automático, ya que proporciona una estimación más confiable del rendimiento del modelo en datos no vistos que simplemente dividir los datos en un conjunto de entrenamiento y un conjunto de prueba. Además, ayuda a reducir el sesgo en la estimación del rendimiento del modelo al utilizar múltiples particiones de los datos.

In [41]:
%%time
# CV evaluation
kfold = KFold(n_splits=10)
scores_reg = []
Xdnn, ydnn = data.drop(columns="mpg"), data.mpg
for train_index, test_index in kfold.split(Xdnn, ydnn):
    # Split data
    X_train, X_test = Xdnn.iloc[train_index], Xdnn.iloc[test_index]
    y_train, y_test = ydnn[train_index], ydnn[test_index]

    normalizer = tf.keras.layers.Normalization(axis=-1)

    normalizer.adapt(np.array(X_train))

    dnn_model = build_and_compile_model(normalizer)

    history = dnn_model.fit(
        X_train,
        y_train,
        validation_split=0.0,
        verbose=0, epochs=100)

    # Predict
    y_pred = dnn_model.predict(X_test).flatten()
    scores_reg += [r2_score(y_test, y_pred)]
2/2 [==============================] - 0s 23ms/step
2/2 [==============================] - 0s 20ms/step
2/2 [==============================] - 0s 34ms/step
WARNING:tensorflow:5 out of the last 10 calls to <function Model.make_predict_function.<locals>.predict_function at 0x7f8c71235800> triggered tf.function retracing. Tracing is expensive and the excessive number of tracings could be due to (1) creating @tf.function repeatedly in a loop, (2) passing tensors with different shapes, (3) passing Python objects instead of tensors. For (1), please define your @tf.function outside of the loop. For (2), @tf.function has reduce_retracing=True option that can avoid unnecessary retracing. For (3), please refer to https://www.tensorflow.org/guide/function#controlling_retracing and https://www.tensorflow.org/api_docs/python/tf/function for  more details.
2/2 [==============================] - 0s 31ms/step
WARNING:tensorflow:6 out of the last 12 calls to <function Model.make_predict_function.<locals>.predict_function at 0x7f8c7014c400> triggered tf.function retracing. Tracing is expensive and the excessive number of tracings could be due to (1) creating @tf.function repeatedly in a loop, (2) passing tensors with different shapes, (3) passing Python objects instead of tensors. For (1), please define your @tf.function outside of the loop. For (2), @tf.function has reduce_retracing=True option that can avoid unnecessary retracing. For (3), please refer to https://www.tensorflow.org/guide/function#controlling_retracing and https://www.tensorflow.org/api_docs/python/tf/function for  more details.
2/2 [==============================] - 0s 27ms/step
2/2 [==============================] - 0s 14ms/step
2/2 [==============================] - 0s 13ms/step
2/2 [==============================] - 0s 34ms/step
2/2 [==============================] - 0s 14ms/step
2/2 [==============================] - 0s 13ms/step
CPU times: user 2min 33s, sys: 2min 26s, total: 4min 59s
Wall time: 1min 43s
In [44]:
data_eval = pd.melt(pd.DataFrame({"MLP": scores, "DNN": scores_reg}))
data_eval.columns = ["Model", "R2"]
sns.boxplot(data=data_eval, x="Model", y="R2", hue="Model")
sns.lineplot(data=data_eval, x='Model', y="R2", err_style="bars", c="orange")
sns.swarmplot(data=data_eval, x='Model', y="R2", c="orange")
plt.plot(data_eval.groupby('Model').mean(), 'Pr')
Out[44]:
[<matplotlib.lines.Line2D at 0x7f8c71fcb4d0>]

DNN para el problema MNIST (clasificación)¶

In [45]:
import tensorflow.keras as keras

(trainX, trainY), (testX, testY) = keras.datasets.mnist.load_data()
Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/mnist.npz
11490434/11490434 [==============================] - 8s 1us/step
In [46]:
from tensorflow.keras import Sequential
from tensorflow.keras.layers import Dense, Input, Flatten

model = Sequential([
          Input(shape=(28,28,1,)),
          Flatten(),
          Dense(units=84, activation="relu"),
          Dense(units=10, activation="softmax"),
      ])

print (model.summary())
Model: "sequential_11"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 flatten (Flatten)           (None, 784)               0         
                                                                 
 dense_33 (Dense)            (None, 84)                65940     
                                                                 
 dense_34 (Dense)            (None, 10)                850       
                                                                 
=================================================================
Total params: 66,790
Trainable params: 66,790
Non-trainable params: 0
_________________________________________________________________
None
In [47]:
model.compile(optimizer="adam", loss=tf.keras.losses.SparseCategoricalCrossentropy(), metrics=["acc"])
In [48]:
history = model.fit(x=trainX, y=trainY, batch_size=256, epochs=10, validation_data=(testX, testY))
Epoch 1/10
235/235 [==============================] - 3s 11ms/step - loss: 9.6618 - acc: 0.7986 - val_loss: 2.2449 - val_acc: 0.8745
Epoch 2/10
235/235 [==============================] - 2s 10ms/step - loss: 1.4748 - acc: 0.8912 - val_loss: 1.1196 - val_acc: 0.8841
Epoch 3/10
235/235 [==============================] - 2s 9ms/step - loss: 0.7915 - acc: 0.8923 - val_loss: 0.7862 - val_acc: 0.8836
Epoch 4/10
235/235 [==============================] - 2s 10ms/step - loss: 0.5146 - acc: 0.9064 - val_loss: 0.6106 - val_acc: 0.9030
Epoch 5/10
235/235 [==============================] - 2s 9ms/step - loss: 0.3894 - acc: 0.9190 - val_loss: 0.5542 - val_acc: 0.9106
Epoch 6/10
235/235 [==============================] - 2s 11ms/step - loss: 0.3073 - acc: 0.9302 - val_loss: 0.5115 - val_acc: 0.9179
Epoch 7/10
235/235 [==============================] - 3s 13ms/step - loss: 0.2540 - acc: 0.9389 - val_loss: 0.4842 - val_acc: 0.9274
Epoch 8/10
235/235 [==============================] - 3s 12ms/step - loss: 0.2121 - acc: 0.9449 - val_loss: 0.4781 - val_acc: 0.9255
Epoch 9/10
235/235 [==============================] - 3s 12ms/step - loss: 0.1923 - acc: 0.9491 - val_loss: 0.4434 - val_acc: 0.9287
Epoch 10/10
235/235 [==============================] - 3s 14ms/step - loss: 0.1692 - acc: 0.9543 - val_loss: 0.4201 - val_acc: 0.9373
In [49]:
plot_loss(history)

Research:

  • Wich metrics are used to evaluate a regression model: https://machinelearningmastery.com/regression-metrics-for-machine-learning/
  • Batch size effect on training: https://medium.com/mini-distill/effect-of-batch-size-on-training-dynamics-21c14f7a716e

Sources:

  • https://www.kaggle.com/code/androbomb/simple-nn-with-python-multi-layer-perceptron
  • https://web.cs.dal.ca/~tt/fundamentals/programs/Octave/Chapter6/
  • https://www.mathworks.com/academia/books/fundamentals-of-computational-neuroscience-trappenberg.html